import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.util.List;
import java.util.ArrayList;
import java.util.Optional;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Iterator;
import java.util.stream.Collectors;

public class Command {
  private final IEntity entity;

  private final IEntity originalEntity;
  private final IAction actions;
  private final List<Player> players = new ArrayList<>();
  private Player current;

  Command(String entityFile, String actionsFile) {
//    this.entityFile = entityFile;
    var actionFactory = new ActionFactory();
    this.entity = initEntityWith(entityFile);
    this.originalEntity = initEntityWith(entityFile);
    this.actions = new FileLoader<IAction>(actionsFile).parse(actionFactory);
  }

  private void findPlayer(List<Token> tokens) {
    if (tokens == null || tokens.isEmpty()) {
      return;
    }
    if (tokens.size() > 1) {
      var colon = tokens.get(1);
      if (colon != null && colon.getType().equals(Token.COLON)) {
        var tok = tokens.get(0);
        var name = tok.getLiteral();
        var playerOrNull = players.stream().filter((p) -> p.getName().equals(name)).findFirst();
        if (playerOrNull.isEmpty()) {
          current = new Player(name, entity);
          players.add(current);
        } else {
          current = playerOrNull.get();
        }
      }
    }
  }

  private IEntity initEntityWith(String file) {
    var entityFactory = new EntityFactory();
    return new FileLoader<IEntity>(file).parse(entityFactory);
  }

  private String reset() {
    entity.processChangedFrom(current);
    current = new Player(current.getName(), entity);
    var index = 0;
    for (; index < players.size(); ++index) {
      if (players.get(index).getName().equals(current.getName())) {
        break;
      }
    }
    players.set(index, current);
    return "You are dead.";
  }

  private void processConsumed(ActionItem action) {
    if (action.getConsumed().contains("health")) return;
    var consumed = Optional.ofNullable(action.getConsumed());
    if (consumed.isPresent()) {
      var currentLocation = current.getCurrentLocation();
      consumed.get().forEach(c -> {
        currentLocation.removeCharacterBy(c);
        currentLocation.removeFurnitureBy(c);
        current.removeArtefactBy(c);
      });
    }
  }

  private void processProduced(ActionItem action) {
    for (var produced : action.getProduced()) {
      var kind = originalEntity.kindOf(produced);
      if (kind != null) {
        if (kind instanceof Cluster) {
          var paths = entity.getPaths();
          paths.addEdge(current.getCurrentLocation().getName(), kind.getName());
        }

        if (kind instanceof Cluster.Furniture) {
          var currentLocation = current.getCurrentLocation();
          currentLocation.getFurniture().add((Cluster.Furniture) kind);
        }

        if (kind instanceof Cluster.Artefact) {
          var currentLocation = current.getCurrentLocation();
          currentLocation.addArtefact((Cluster.Artefact) kind);
        }

        if (kind instanceof Cluster.Character) {
          var currentLocation = current.getCurrentLocation();
          currentLocation.getCharacters().add((Cluster.Character) kind);
        }
      }
    }
  }

  private String processHealth(ActionItem action) {
    if (action.getConsumed().contains("health")) {
      var health = current.getHealth() - 1;
      current.setHealth(health);
      if (health < 1) {
        return reset();
      }
    } else if (action.getProduced().contains("health")) {
      var health = current.getHealth() + 1;
      if (health <= 3) {
        current.setHealth(health);
      }
    }
    return action.getNarration();
  }

  private final String ARTEFACT = "ARTEFACT";
  private final String CHARACTER = "CHARACTER";

  private boolean viewSubject(Subgraph kind1, Subgraph kind2) {
    final String FURNITURE = "FURNITURE";
    var kindString1 = kind1 != null ? kind1.getKindString() : "";
    var kindString2 = kind2 != null ? kind2.getKindString() : "";
    if ((kindString1.equals(FURNITURE) || kindString1.equals(CHARACTER)) && kindString2.equals(ARTEFACT)) {
      boolean a = false, b = false;
      if (kind1 instanceof Cluster.Furniture) {
        a = current.peekCurrentLocationHaveFurnitureBy(kind1.getName());
      } else if (kind1 instanceof Cluster.Character) {
        a = current.peekCurrentLocationHaveCharacterBy(kind1.getName());
      }
      if (kind2 instanceof Cluster.Artefact) {
        b = current.peekHaveArtefact(kind2.getName());
      }
      return (a && b);
    }
    return false;
  }

  private String getCommandBy(String trigger, ActionItem action) {
    var subjects = action.getSubjects();
    var command = "";
    if (subjects.size() > 1) {
      var subject1 = subjects.get(0);
      var subject2 = subjects.get(1);
      var kind1 = originalEntity.kindOf(subject1);
      var kind2 = originalEntity.kindOf(subject2);
      if (viewSubject(kind1, kind2)) {
        command = trigger + " " + subject1 + " with " + subject2;
      }
      if (viewSubject(kind2, kind1)) {
        command = trigger + " " + subject2 + " with " + subject1;
      }
    } else if (subjects.size() == 1) {
      var subject = subjects.get(0);
      var kind = originalEntity.kindOf(subject);
      var kindString = kind.getKindString();
      if (ARTEFACT.equals(kindString) || CHARACTER.equals(kindString)) {
        boolean a = false;
        if (kind instanceof Cluster.Character) {
          a = current.peekCurrentLocationHaveCharacterBy(kind.getName());
        } else if (kind instanceof Cluster.Artefact) {
          a = current.peekHaveArtefact(kind.getName());
        }
        if (a) {
          command = trigger + " " + subject;
        }
      }
    }
    return command;
  }

  private String triggerWith(String trigger, ActionItem action, Queue<Token> tokens) {
    var userInput = trigger + " " + tokens.stream().map(Token::getLiteral).collect(Collectors.joining(" "));
    var command = getCommandBy(trigger, action);
    if (command.equals(userInput)) {
      processConsumed(action);
      processProduced(action);
      return processHealth(action);
    }

    return "Can not trigger action: " + userInput;
  }

  private String triggerAction(String trigger, Iterator<Token> iterator) {
    var action = actions.findActionBy(trigger);
    var result = "";
    if (Optional.ofNullable(action).isPresent()) {
      Queue<Token> tokens = new LinkedList<>();
      while (iterator.hasNext()) {
        tokens.add(iterator.next());
      }
      result = triggerWith(trigger, action, tokens);
    }
    return result;
  }

  private String exec(List<Token> tokens) {
    var result = "";
    if (tokens == null || tokens.isEmpty()) {
      return result;
    }
    var iterator = tokens.iterator();
    if (iterator.hasNext()) iterator.next();
    if (iterator.hasNext()) iterator.next();

    if (iterator.hasNext()) {
      var command = iterator.next();
      var currentType = command.getType();
      if (currentType.equals(Token.TRIGGER)) {
        result = triggerAction(command.getLiteral(), iterator);
      } else if (currentType.equals(Token.BUILTIN)) {
        var opcode = "";
        if (iterator.hasNext()) {
          var t = iterator.next();
          opcode = t.getLiteral();
        }
        result = BuiltIn
            .valueOf(command.getLiteral().toUpperCase())
            .op(current, opcode, entity);
      }
    }
    return result;
  }

  public void parse(BufferedReader in, BufferedWriter out) throws IOException {
    String line = in.readLine();
    var lexer = new Lexer(line, actions);
    var tokens = lexer.getTokens();
    findPlayer(tokens);
    if (current != null) {
      var command = exec(tokens);
      out.write(command + "\n");
    }
  }
}
